查看原文
其他

深入浅出 NavigationUI | MAD Skills

Android Android 开发者 2021-10-12

这是第二个关于导航 (Navigation) 的 MAD Skills 系列,如果您想回顾过去发布的内容,请参考下面链接查看:


今天为大家发布本系列文章中的第一篇。在本文中,我们将为大家讲解另外一个用例,即类似操作栏 (Action Bar)、底部标签栏或者抽屉型导航栏之类的 UI 组件如何在应用中实现导航功能。如果您更倾向于观看视频而非阅读文章,请查看以下视频内容:

△ 深入了解 NavigationUI



概述



在之前的导航系列文章中,Chet 开发了一个用于跟踪甜甜圈的应用。知道什么是甜甜圈的最佳搭档吗?(难道是另一个甜甜圈?) 当然是咖啡!所以我准备增加一个追踪咖啡的功能。我需要在应用中增加一些页面,所以有必要使用抽屉式导航栏或者底部标签栏来辅助用户导航。但是我们该如何使用这些 UI 组件来集成导航功能呢?通过点击监听器手动触发导航动作吗?


  • Chet
    https://chethaase.medium.com/
  • 跟踪甜甜圈的应用
    https://github.com/android/architecture-components-samples/tree/main/MADSkillsNavigationSample

不需要!无需任何监听器。NavigationUI 类通过匹配目标页面 id 与菜单 id 实现不同页面之间的导航功能。让我们深入探索一下它的内部机制吧。



添加咖啡追踪器



△ 工程结构

首先我将与甜甜圈相关的类文件拷贝了一份到新的包下,并且将它们重命名。这样的操作对于真正的应用来说也许不是最好的做法,但是在这里可以快速帮助我们添加咖啡跟踪功能到已有的应用中。如果您希望随着文章内容同步操作,可以获取这里的代码,里面包含了全部针对 Donut Tracker 应用的修改,可以基于该代码了解 NavigationUI


  • 代码
    https://github.com/google-developer-training/android-demos/tree/starter


基于上面所做的修改,我更新了导航图,新增了从 coffeeFragmentcoffeeDialogFragment 以及从 selectionFragmentdonutFragment 相关的目的页面和操作。之后我会用到这些目的页面的 id ;)

△ 带有新的目的页面的导航图

更新导航图之后,我们可以开始将元素绑定起来,并且实现导航到 SelectionFragment



选项菜单



应用的选项菜单现在尚未发挥作用。要启用它,需要在 onOptionsItemSelected() 函数中,为被选择的菜单项调用 onNavDestinationSelected() 函数,并传入 navController。只要目的页面的 idMenuItemid 相匹配,该函数会导航到绑定在 MenuItem 上的目的页面。

override fun onOptionsItemSelected(item: MenuItem): Boolean { return item.onNavDestinationSelected( findNavController(R.id.nav_host_fragment) ) || super.onOptionsItemSelected(item)}

现在导航控制器可以 "支配" 菜单项了,我将 MenuItemid 与之前所创建的目的页面的 id 进行了匹配。这样,导航组件就可以将 MenuItem 与目的页面进行关联。

<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" tools:context="com.android.samples.donuttracker.MainActivity"> <item android:id="@+id/selectionFragment" android:orderInCategory="100" android:title="@string/action_settings" app:showAsAction="never" /></menu>


Toolbar



现在应用可以导航到 selectionFragment,但是标题仍然保持原样。当处于 selectionFragment 的时候,我们希望标题可以被更新并且显示返回按钮。


首先我需要添加一个 AppBarConfiguration 对象,NavigationUI 会使用该对象来管理应用左上角的导航按钮的行为。

appBarConfiguration = AppBarConfiguration(navController.graph)

该按钮会根据您的目的页面的层级改变自身的行为。比如,当您在最顶层的目的页面时,就不会显示回退按钮,因为没有更高层级的页面。


默认情况下,您应用的最初页面是唯一的最顶层目的页面,但是您也可以定义多个最顶层目的页面。比如,在我们的应用中,我可以将 donutListcoffeeList 的目的页面都定义为最顶层的目的页面。

接下来,在 MainActivity 类中,获得 navControllertoolbar 的实例,并且验证 setSupportActionBar() 是否被调用。这里我还更新了传入函数的 toolbar 的引用。

val navHostFragment = supportFragmentManager.findFragmentById( R.id.nav_host_fragment) as NavHostFragmentnavController = navHostFragment.navControllerval toolbar = binding.toolbar

要在默认的操作栏 (Action Bar) 中添加导航功能,我在这里使用了 setupActionBarWithNavController() 函数。该函数需要两个参数: navControllerappBarConfiguration

setSupportActionBar(toolbar)setupActionBarWithNavController(navController, appBarConfiguration)


接下来,根据目前的目的页面,我覆写了 onSupportNavigationUp() 函数,然后在 nav_host_fragment 上调用 navigateUp() 并传入 appBarConfiguration 来支持回退导航或者显示菜单图标的功能。

override fun onSupportNavigateUp(): Boolean { return findNavController(R.id.nav_host_fragment).navigateUp( appBarConfiguration )}

现在我可以导航到 selectionFragment,并且您可以看到标题已经更新,并且也显示了返回按钮,用户可以返回到之前的页面。

△ 标题更新了并且也显示了返回按钮


底部标签栏



目前为止还算顺利,但是应用还不能导航到 coffeeList Fragment。接下来我们将解决这个问题。


我们从添加底部标签栏入手。首先添加 bottom_nav_menu.xml 文件并且声明两个菜单元素。NavigationUI 依赖 MenuItemid,用它与导航图中目的页面的 id 进行匹配。我还为每个目的页面设置了图标和标题。

<menu xmlns:android="http://schemas.android.com/apk/res/android"> <item android:id="@id/donutList" android:icon="@drawable/donut_with_sprinkles" android:title="@string/donut_name" /> <item android:id="@id/coffeeList" android:icon="@drawable/coffee_cup" android:title="@string/coffee_name" /></menu>

现在 MenuItem 已经就绪,我在 mainActivity 的布局中添加了 BottomNavigationView,并且将 bottom_nav_menu 设置为 BottomNavigationViewmenu 属性。

<com.google.android.material.bottomnavigation.BottomNavigationView android:id="@+id/bottom_nav_view" android:layout_width="match_parent" android:layout_height="wrap_content"       app:menu="@menu/bottom_nav_menu" />


要使底部标签栏发挥作用,这里调用 setupWithNavController() 函数将 navController 传入 BottomNavigationView

private fun setupBottomNavMenu(navController: NavController) { val bottomNav = findViewById<BottomNavigationView>( R.id.bottom_nav_view ) bottomNav?.setupWithNavController(navController)}

请注意我并没有从导航图中调用任何导航操作。实际上导航图中甚至没有前往 coffeeList Fragment 的路径。和之前对 ActionBar 所做的操作一样,BottomNavigationView 通过匹配 MenuItemid 和导航目的页面的 id 来自动响应导航操作。



抽屉式导航栏



虽然看上去不错,但是如果您设备的屏幕尺寸较大,那么底部标签栏恐怕无法提供最佳的用户体验。要解决这个问题,我会使用另外一个布局文件,它带有 w960dp 限定符,表明它适用于屏幕更大、更宽的设备。


这个布局文件与默认的 activity_main 布局相类似,其中已经包含了 ToolbarFragmentContainerView。我需要添加 NavigationView,并且将 nav_drawer_menu 设置为 NavigationViewmenu 属性。接下来,我将在 NavigationViewFragmentContainerView 之间添加分隔符。

<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.android.samples.donuttracker.MainActivity"> <com.google.android.material.navigation.NavigationView android:id="@+id/nav_view" android:layout_width="wrap_content" android:layout_height="match_parent" android:layout_alignParentStart="true" app:elevation="0dp" app:menu="@menu/nav_drawer_menu" /> <View android:layout_width="1dp" android:layout_height="match_parent" android:layout_toEndOf="@id/nav_view" android:background="?android:attr/listDivider" /> <androidx.appcompat.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:background="@color/colorPrimary" android:layout_toEndOf="@id/nav_view" android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar" /> <androidx.fragment.app.FragmentContainerView android:id="@+id/nav_host_fragment" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="match_parent" android:layout_height="match_parent" android:layout_below="@id/toolbar" app:defaultNavHost="true" android:layout_toEndOf="@id/nav_view" app:navGraph="@navigation/nav_graph" /></RelativeLayout>

如此一来,在宽屏幕设备上,NavigationView 会代替 BottomNavigationView 显示在屏幕上。现在布局文件已经就绪,我再创建一个 nav_drawer_menu.xml,并且将 donutListcoffeeList 作为主要的分组添加为目的页面。对于 MenuItem,我添加了 selectionFragment 作为它的目的页面。

<menu xmlns:android="http://schemas.android.com/apk/res/android"> <group android:id="@+id/primary"> <item android:id="@id/donutList" android:icon="@drawable/donut_with_sprinkles" android:title="@string/donut_name" /> <item android:id="@id/coffeeList" android:icon="@drawable/coffee_cup" android:title="@string/coffee_name" /> </group> <item android:id="@+id/selectionFragment" android:title="@string/action_settings" /></menu>

现在所有布局已经就绪,我们回到 MainActivity,设置抽屉式导航栏,使其能够与 NavigationController 协作。和之前针对 BottomNavigationView 所做的相类似,这里创建一个新的方法,并且调用 setupWithNavController() 函数将 navController 传入 NavigationView为了使代码保持整洁、各个元素之间更加清晰,我们会在新的方法中实现相关操作,并且在 onCreate() 中调用该方法。
private fun setupNavigationMenu(navController: NavController){ val sideNavView = findViewById<NavigationView>(R.id.nav_view) sideNavView?.setupWithNavController(navController)}

现在当我在屏幕较宽的设备上运行应用时,可以看到抽屉式导航栏已经设置了 MenuItem,并且在导航图中,MenuItem 和目的页面的 id 是相匹配的。

△ 在屏幕较宽的设备上运行 Donut Tracker

请注意,当我切换页面的时候返回按钮会自动显示在左上角。如果您想这么做,还可以修改 AppBarConfiguration 来将 CoffeeList 添加为最顶层的目的页面。



小结



本次分享的内容就是这些了。Donut Tracker 应用并不需要底部标签栏或者抽屉式导航栏,但是添加了新的功能和目的页面后,NavigationUI 可以很大程度上帮助我们处理应用中的导航功能。


我们无需进行多余的操作,仅需添加 UI 组件,并且匹配 MenuItemid 和目的页面的 id。您可以查阅完整代码,并且通过 main 与 starter 分支的比较,观察代码的变化。


  • 完整代码
    https://github.com/google-developer-training/android-demos/tree/main/DonutTracker/NavigationUI
  • 比较
    https://github.com/google-developer-training/android-demos/compare/main...starter



 点击屏末 | 阅读原文 | 即刻了解如何使用 NavigationUI 更新界面组件


推荐阅读

如页面未加载,请刷新重试


视频 小程序 ,轻点两下取消赞 在看 ,轻点两下取消在看

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存